/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../helper/Validate'
import { StringUtil } from '../internal/StringUtil'
import { Attributes } from './Attributes'
import { Syntax } from './Syntax'
import {OutputSettings} from './OutputSettings'
import { Entities } from './Entities'
import hilog from '@ohos.hilog'
import { ArrayUtils } from '../util/ArrayUtils'
import { Normalizer } from '../internal/Normalizer'
import logUtil from '../util/LogUtil'

export class Attribute extends Map<string, string> {
  private static readonly _booleanAttributes: string[] = ["allowfullscreen", "async", "autofocus", "checked", "compact", "declare", "default", "defer", "disabled",
    "formnovalidate", "hidden", "inert", "ismap", "itemscope", "multiple", "muted", "nohref", "noresize",
    "noshade", "novalidate", "nowrap", "open", "readonly", "required", "reversed", "seamless", "selected",
    "sortable", "truespeed", "typemustmatch"]
  private _key: String
  private _val: String
  // used to update the holding Attributes when the key / value is changed via this interface
  parent: Attributes

  constructor(key: String, val: String, parent: Attributes = null) {
    super()
    Validate.notNull(key)
    key = key.trim()
    // trimming could potentially make empty, so validate here
    Validate.notEmpty(key,'Attribute key must not be empty')
    this._key = key
    this._val = val
    this.parent = parent
  }

  /**
   Get the attribute key.
   @return the attribute key
   */
  public getKey(): string {
    return this._key.toString()
  }

  /**
   Set the attribute key; case is preserved.
   @param key the new key; must not be null
   */
  public setKey(key: string) {
    Validate.notNull(key)
    key = key.trim()
    // trimming could potentially make empty, so validate here
    Validate.notEmpty(key,'Attribute setKey key must not be empty')
    if (this.parent !== undefined && this.parent !== null) {
      let i = this.parent.indexOfKey(this._key.toString())
      if (i !== Attributes.NotFound) {
        this.parent.keys[i] = key
      }
    }
    this._key = key
  }

  /**
   Get the attribute value. Will return an empty string if the value is not set.
   @return the attribute value
   */
  public getValue(): string {
    return Attributes.checkNotNull(this._val)
  }

  /**
   * Check if this Attribute has a value. Set boolean attributes have no value.
   * @return if this is a boolean attribute / attribute without a value
   */
  public hasDeclaredValue(): boolean {
    return this._val !== undefined && this._val !== null;
  }

  /**
   Set the attribute value.
   @param val the new attribute value; must not be null
   */
  public setValue(val: string): string {
    let oldVal = this._val;
    if (this.parent !== undefined && this.parent !== null) {
      // trust the container more
      oldVal = this.parent.get(this._key.toString());
      let i = this.parent.indexOfKey(this._key.toString());
      if (i != Attributes.NotFound) {
        this.parent.vals[i] = val;
      }

    }
    this._val = val;
    return Attributes.checkNotNull(oldVal);
  }

  /**
   Get the HTML representation of this attribute; e.g. {@code href="index.html"}.
   @return HTML
   */
  public html(): string {
    let sb = StringUtil.borrowBuilder()
    try {
      // this.html1(sb, new Document("").getOutputSettings())
      //FIXME
      this.html1(sb, new OutputSettings())
    } catch (e) {
    }
    return StringUtil.releaseBuilder(sb)
  }

  html1(accum: string[], out: OutputSettings) {
    let val = (this._val === undefined || this._val === null) ? null:this._val.toString()
    Attribute.html(this._key.toString(), val, accum, out);
  }

  static html(key: string, val: string, accum: string[], out: OutputSettings) {
    key = Attribute.getValidKey(key, out.getSyntax())
    hilog.info(0x0000, 'testTag', '%{public}s', `html:${key === undefined || key === null}`);
    if (key === undefined || key === null) {
      return
    }
    Attribute.htmlNoValidate(key, val, accum, out)
  }

  static htmlNoValidate(key:string, val:string,accum: string[], out: OutputSettings) {
    // structured like this so that Attributes can check we can write first, so it can add whitespace correctly
    accum.push(key);
    if (!Attribute.shouldCollapseAttribute(key, val, out)) {
      accum.push("=\"");
      Entities.escape7(accum, Attributes.checkNotNull(val), out, true, false, false,false);
      accum.push('"');
    } else {
      hilog.info(0x0000, 'testTag', '%{public}s', `htmlNoValidate else`);
    }
  }

  private static readonly  xmlKeyValid:RegExp = /[a-zA-Z_:][-a-zA-Z0-9_:.]*/;
  private static readonly xmlKeyReplace:RegExp = /[^-a-zA-Z0-9_:.]/g;
  private static readonly htmlKeyValid:RegExp = /^[^\x00-\x1f\x7f-\x9f "'/=]+$/;
  private static readonly htmlKeyReplace:RegExp = /[\x00-\x1f\x7f-\x9f "'/=]/g;

  public static getValidKey(key: string, syntax: Syntax): string | null {
    //  we consider HTML attributes to always be valid. XML checks key validity
    if (syntax === Syntax.xml && !Attribute.xmlKeyValid.test(key)) {
      key = key.replace(Attribute.xmlKeyReplace, "")
      // null if could not be coerced
      return Attribute.xmlKeyValid.test(key) ? key : null
    } else if (syntax === Syntax.html ) {
      if (!Attribute.htmlKeyValid.test(key)) {
        key = key.replace(Attribute.htmlKeyReplace, "")
        // null if could not be coerced
        return Attribute.htmlKeyValid.test(key) ? key : null
      }
    }
    return key;
  }


  /**
   Get the string representation of this attribute, implemented as {@link #html()}.
   @return string
   */
  public toString(): string {
    return this.html();
  }

  /**
   * Create a new Attribute from an unencoded key and a HTML attribute encoded value.
   * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars.
   * @param encodedValue HTML attribute encoded value
   * @return attribute
   */
  public static createFromEncoded(unencodedKey: string, encodedValue: string): Attribute {
    let value = Entities.unescape(encodedValue, true);
    // parent will get set when Put
    return new Attribute(unencodedKey, value, null);
  }

  isDataAttribute(): boolean {
    return Attribute.isDataAttribute(this._key.toString());
  }

  static isDataAttribute(key: string): boolean {
    return key.startsWith(Attributes.dataPrefix) && key.length > Attributes.dataPrefix.length;
  }

  /**
   * Collapsible if it's a boolean attribute and value is empty or same as name
   *
   * @param out output settings
   * @return Returns whether collapsible or not
   */
  shouldCollapseAttribute(out: OutputSettings): boolean {
    return Attribute.shouldCollapseAttribute(this._key.toString(), this._val.toString(), out)
  }

  static shouldCollapseAttribute(key: string, val: string|null, out: OutputSettings): boolean {
    let a = out.getSyntax() === Syntax.html
    let b = (val === undefined || val === null) || (val.length === 0 || val.toLowerCase() === key.toLowerCase()) && Attribute.isBooleanAttribute(key)
    hilog.info(0x0000, 'testTag', '%{public}s', `shouldCollapseAttribute a:${a}  b:${b}`);
    return a && b
  }

  /**
   * Checks if this attribute name is defined as a boolean attribute in HTML5
   */
  static isBooleanAttribute(key: string): boolean {
    // return Attribute._booleanAttributes.findIndex(item => item === key) >= 0
    return ArrayUtils.binarySearch(Attribute._booleanAttributes, Normalizer.lowerCase(key)) >= 0
  }

  public equals(o: any): boolean { // note parent not considered
    if (this === o) return true;
    if (o === undefined || o === null) return false;
    let attribute = o as Attribute;
    if ((this._key !== undefined && this._key !== null) ? this._key !== attribute._key : attribute._key !== undefined && attribute._key !== null) return false;
    return (this._val !== undefined && this._val !== null) ? this._val === attribute._val : attribute._val === undefined || attribute._val === null;
  }

  public hashCode(): number { // note parent not considered
    let result = this._key !== undefined && this._key !== null ? this._key.charCodeAt(0) : 0;
    result = 31 * result + ((this._val !== undefined && this._val !== null) ? this._val.charCodeAt(0) : 0);
    return result;
  }


  public clone(): Attribute {
      try {
        let clone = new Attribute("","")
        return Object.assign(clone, this);
      } catch (e) {
      }
    return null
  }
}

class StringWrapper {
  constructor(public value: string){}
}